#!/usr/bin/sh
#
# shellsnoop - A program to print read/write details from shells,
#	       such as keystrokes and command outputs.
#	       Written using DTrace (Solaris 10 3/05).
#
# This program sounds somewhat dangerous (snooping keystrokes), but is
# no more so than /usr/bin/truss, and both need root or dtrace privileges to
# run. In fact, less dangerous, as we only print visible text (not password
# text, for example). Having said that, it goes without saying that this
# program shouldn't be used for breeching privacy of other users.
#
# This was written as a tool to demonstrate the capabilities of DTrace.
#
# $Id: shellsnoop 19 2007-09-12 07:47:59Z brendan $
#
# USAGE:	shellsnoop [-hqsv] [-p PID] [-u UID]
#
#		-q		# quiet, only print data
#		-s		# include start time, us
#		-v		# include start time, string
#		-p PID		# process ID to snoop
#		-u UID		# user ID to snoop
#  eg,
#		shellsnoop		# default output
#		shellsnoop -v		# human readable timestamps
#		shellsnoop -p 1892	# snoop this PID only
#		shellsnoop -qp 1892	# watch this PID data only
# 	
# FIELDS:
#		UID		User ID
#		PID		process ID
#		PPID		parent process ID
#		COMM		command name
#		DIR		direction (R read, W write)
#		TEXT		text contained in the read/write
#		TIME		timestamp for the command, us
#		STRTIME		timestamp for the command, string
#
# SEE ALSO: ttywatcher
#
# COPYRIGHT: Copyright (c) 2005 Brendan Gregg.
#
# CDDL HEADER START
#
#  The contents of this file are subject to the terms of the
#  Common Development and Distribution License, Version 1.0 only
#  (the "License").  You may not use this file except in compliance
#  with the License.
#
#  You can obtain a copy of the license at Docs/cddl1.txt
#  or http://www.opensolaris.org/os/licensing.
#  See the License for the specific language governing permissions
#  and limitations under the License.
#
# CDDL HEADER END
#
# Author: Brendan Gregg  [Sydney, Australia]
#
# 28-Mar-2004	Brendan Gregg	Created this.
# 21-Jan-2005	   "	  "	Wrapped in sh to provide options.
# 30-Nov-2005	   "	  "	Fixed trailing buffer text bug.
# 30-Nov-2005	   "	  "	Fixed sh no keystroke text in quiet bug.
# 30-Nov-2005	   "	  "	Last update.
# 


##############################
# --- Process Arguments ---
#
opt_pid=0; opt_uid=0; opt_time=0; opt_timestr=0; opt_quiet=0; opt_debug=0
filter=0; pid=0; uid=0

while getopts dhp:qsu:v name
do
	case $name in
	d)	opt_debug=1 ;;
	p)	opt_pid=1; pid=$OPTARG ;;
	q)	opt_quiet=1 ;;
	s)	opt_time=1 ;;
	u)	opt_uid=1; uid=$OPTARG ;;
	v)	opt_timestr=1 ;;
	h|?)	cat <<-END >&2
		USAGE: shellsnoop [-hqsv] [-p PID] [-u UID]
		       shellsnoop		# default output
		                -q		# quiet, only print data
		                -s		# include start time, us
		                -v		# include start time, string
		                -p PID		# process ID to snoop
		                -u UID		# user ID to snoop
		END
		exit 1
	esac
done

if [ $opt_quiet -eq 1 ]; then
	opt_time=0; opt_timestr=0
fi
if [ $opt_pid -eq 1 -o $opt_uid -eq 1 ]; then
	filter=1
fi


#################################
# --- Main Program, DTrace ---
#
dtrace -n '
 /*
  * Command line arguments
  */
 inline int OPT_debug 	= '$opt_debug';
 inline int OPT_quiet 	= '$opt_quiet';
 inline int OPT_pid 	= '$opt_pid';
 inline int OPT_uid 	= '$opt_uid';
 inline int OPT_time 	= '$opt_time';
 inline int OPT_timestr	= '$opt_timestr';
 inline int FILTER 	= '$filter';
 inline int PID 	= '$pid';
 inline int UID 	= '$uid';
 
 #pragma D option quiet
 #pragma D option switchrate=20hz
 
 /*
  * Print header
  */
 dtrace:::BEGIN /OPT_time == 1/
 { 
 	printf("%-14s ","TIME");
 }
 dtrace:::BEGIN /OPT_timestr == 1/
 { 
 	printf("%-20s ","STRTIME");
 }
 dtrace:::BEGIN /OPT_quiet == 0/
 {
	printf("%5s %5s %8s %3s  %s\n", "PID", "PPID", "CMD", "DIR", "TEXT");
 }

 /*
  * Remember this PID is a shell child
  */
 syscall::exec:entry, syscall::exece:entry
 /execname == "sh"   || execname == "ksh"  || execname == "csh"  || 
  execname == "tcsh" || execname == "zsh"  || execname == "bash"/
 {
	child[pid] = 1;
 
	/* debug */
	this->parent = (char *)curthread->t_procp->p_parent->p_user.u_comm;
	OPT_debug == 1 ? printf("PID %d CMD %s started. (%s)\n",
	    pid, execname, stringof(this->parent)) : 1;
 }
 syscall::exec:entry, syscall::exece:entry
 /(OPT_pid == 1 && PID != ppid) || (OPT_uid == 1 && UID != uid)/
 {
	/* forget if filtered */
	child[pid] = 0;
 }

 /*
  * Print shell keystrokes
  */
 syscall::write:entry, syscall::read:entry
 /(execname == "sh"   || execname == "ksh"  || execname == "csh"  ||
  execname == "tcsh" || execname == "zsh"  || execname == "bash")
  && (arg0 >= 0 && arg0 <= 2)/
 {
	self->buf = arg1;
 }
 syscall::write:entry, syscall::read:entry
 /(OPT_pid == 1 && PID != pid) || (OPT_uid == 1 && UID != uid)/
 {
	self->buf = 0;
 }
 syscall::write:return, syscall::read:return
 /self->buf && child[pid] == 0 && OPT_time == 1/
 {
 	printf("%-14d ", timestamp/1000);
 }
 syscall::write:return, syscall::read:return
 /self->buf && child[pid] == 0 && OPT_timestr == 1/
 {
	printf("%-20Y ", walltimestamp);
 }
 syscall::write:return, syscall::read:return
 /self->buf && child[pid] == 0 && OPT_quiet == 0/
 {
	this->text = (char *)copyin(self->buf, arg0);
	this->text[arg0] = '\'\\0\'';
 
	printf("%5d %5d %8s %3s  %s\n", pid, curpsinfo->pr_ppid, execname, 
	    probefunc == "read" ? "R" : "W", stringof(this->text));
 }
 syscall::write:return
 /self->buf && child[pid] == 0 && OPT_quiet == 1/
 {
	this->text = (char *)copyin(self->buf, arg0);
	this->text[arg0] = '\'\\0\'';
	printf("%s", stringof(this->text));
 }
 syscall::read:return
 /self->buf && execname == "sh" && child[pid] == 0 && OPT_quiet == 1/
 {
	this->text = (char *)copyin(self->buf, arg0);
	this->text[arg0] = '\'\\0\'';
	printf("%s", stringof(this->text));
 }
 syscall::write:return, syscall::read:return
 /self->buf && child[pid] == 0/
 {
	self->buf = 0;
 }

 /*
  * Print command output
  */
 syscall::write:entry, syscall::read:entry
 /child[pid] == 1 && (arg0 == 1 || arg0 == 2)/
 {
	self->buf = arg1;
 }
 syscall::write:return, syscall::read:return
 /self->buf && OPT_time == 1/
 {
 	printf("%-14d ", timestamp/1000);
 }
 syscall::write:return, syscall::read:return
 /self->buf && OPT_timestr == 1/
 {
	printf("%-20Y ", walltimestamp);
 }
 syscall::write:return, syscall::read:return
 /self->buf && OPT_quiet == 0/
 {
	this->text = (char *)copyin(self->buf, arg0);
	this->text[arg0] = '\'\\0\'';
 
	printf("%5d %5d %8s %3s  %s", pid, curpsinfo->pr_ppid, execname,
	    probefunc == "read" ? "R" : "W", stringof(this->text));
 
	/* here we check if a newline is needed */
	this->length = strlen(this->text);
	printf("%s", this->text[this->length - 1] == '\'\\n\'' ? "" : "\n");
	self->buf = 0;
 }
 syscall::write:return, syscall::read:return
 /self->buf && OPT_quiet == 1/
 {
	this->text = (char *)copyin(self->buf, arg0);
	this->text[arg0] = '\'\\0\'';
	printf("%s", stringof(this->text));
	self->buf = 0;
 }

 /*
  *  Cleanup
  */
 syscall::rexit:entry
 {
	child[pid] = 0;

	/* debug */
	this->parent = (char *)curthread->t_procp->p_parent->p_user.u_comm;
	OPT_debug == 1 ? printf("PID %d CMD %s exited. (%s)\n",
	 pid, execname, stringof(this->parent)) : 1;
 }
'
